import Sortable from 'sortablejs'
import { insertNodeAt, removeNode } from './util/htmlHelper'
import { console } from './util/console'
import {
  getComponentAttributes,
  createSortableOption,
  getValidSortableEntries
} from './core/componentBuilderHelper'
import { computeComponentStructure } from './core/renderHelper'
import { events } from './core/sortableEvents'
import { h, defineComponent, nextTick } from 'vue'

function emit(evtName, evtData) {
  nextTick(() => this.$emit(evtName.toLowerCase(), evtData))
}

function manage(evtName) {
  return (evtData, originalElement) => {
    if (this.realList !== null) {
      return this[`onDrag${evtName}`](evtData, originalElement)
    }
  }
}

function manageAndEmit(evtName) {
  const delegateCallBack = manage.call(this, evtName)
  return (evtData, originalElement) => {
    delegateCallBack.call(this, evtData, originalElement)
    emit.call(this, evtName, evtData)
  }
}

let draggingElement = null

const props = {
  list: {
    type: Array,
    required: false,
    default: null
  },
  modelValue: {
    type: Array,
    required: false,
    default: null
  },
  itemKey: {
    type: [String, Function],
    required: true
  },
  clone: {
    type: Function,
    default: (original) => {
      return original
    }
  },
  tag: {
    type: String,
    default: 'div'
  },
  move: {
    type: Function,
    default: null
  },
  componentData: {
    type: Object,
    required: false,
    default: null
  },
  componentSlots: {
    //不能包含default
    type: Object,
    default: null
  }
}

const emits = [
  'update:modelValue',
  'change',
  ...[...events.manageAndEmit, ...events.emit].map((evt) => evt.toLowerCase())
]

const draggableComponent = defineComponent({
  name: 'draggable',

  inheritAttrs: false,

  props,

  emits,

  data() {
    return {
      error: false
    }
  },

  render() {
    try {
      this.error = false
      const {
        $slots,
        $attrs,
        tag,
        componentData,
        realList,
        getKey,
        componentSlots
      } = this
      const componentStructure = computeComponentStructure({
        $slots,
        tag,
        realList,
        getKey
      })
      this.componentStructure = componentStructure
      const attributes = getComponentAttributes({ $attrs, componentData })
      return componentStructure.render(h, attributes, componentSlots)
    } catch (err) {
      this.error = true
      return h('pre', { style: { color: 'red' } }, err.stack)
    }
  },

  created() {
    if (this.list !== null && this.modelValue !== null) {
      console.error(
        'modelValue and list props are mutually exclusive! Please set one or another.'
      )
    }
  },

  mounted() {
    if (this.error) {
      return
    }

    const { $attrs, $el, componentStructure } = this
    componentStructure.updated()

    const sortableOptions = createSortableOption({
      $attrs,
      callBackBuilder: {
        manageAndEmit: (event) => manageAndEmit.call(this, event),
        emit: (event) => emit.bind(this, event),
        manage: (event) => manage.call(this, event)
      }
    })
    const targetDomElement = $el.nodeType === 1 ? $el : $el.parentElement
    this._sortable = new Sortable(targetDomElement, sortableOptions)
    this.targetDomElement = targetDomElement
    targetDomElement.__draggable_component__ = this
  },

  updated() {
    this.componentStructure.updated()
  },

  beforeUnmount() {
    if (this._sortable !== undefined) this._sortable.destroy()
  },

  computed: {
    realList() {
      const { list } = this
      return list ? list : this.modelValue
    },

    getKey() {
      const { itemKey } = this
      if (typeof itemKey === 'function') {
        return itemKey
      }
      return (element) => element[itemKey]
    }
  },

  watch: {
    $attrs: {
      handler(newOptionValue) {
        const { _sortable } = this
        if (!_sortable) return
        getValidSortableEntries(newOptionValue).forEach(([key, value]) => {
          _sortable.option(key, value)
        })
      },
      deep: true
    }
  },

  methods: {
    getUnderlyingVm(domElement) {
      return this.componentStructure.getUnderlyingVm(domElement) || null
    },

    getUnderlyingPotencialDraggableComponent(htmElement) {
      //TODO check case where you need to see component children
      return htmElement.__draggable_component__
    },

    emitChanges(evt) {
      nextTick(() => this.$emit('change', evt))
    },

    alterList(onList) {
      if (this.list) {
        onList(this.list)
        return
      }
      const newList = [...this.modelValue]
      onList(newList)
      this.$emit('update:modelValue', newList)
    },

    spliceList() {
      const spliceList = (list) => list.splice(...arguments)
      this.alterList(spliceList)
    },

    updatePosition(oldIndex, newIndex) {
      const updatePosition = (list) =>
        list.splice(newIndex, 0, list.splice(oldIndex, 1)[0])
      this.alterList(updatePosition)
    },

    getRelatedContextFromMoveEvent({ to, related }) {
      const component = this.getUnderlyingPotencialDraggableComponent(to)
      if (!component) {
        return { component }
      }
      const list = component.realList
      const context = { list, component }
      if (to !== related && list) {
        const destination = component.getUnderlyingVm(related) || {}
        return { ...destination, ...context }
      }
      return context
    },

    getVmIndexFromDomIndex(domIndex) {
      return this.componentStructure.getVmIndexFromDomIndex(
        domIndex,
        this.targetDomElement
      )
    },

    onDragStart(evt) {
      this.context = this.getUnderlyingVm(evt.item)
      evt.item._underlying_vm_ = this.clone(this.context.element)
      draggingElement = evt.item
    },

    onDragAdd(evt) {
      const element = evt.item._underlying_vm_
      if (element === undefined) {
        return
      }
      removeNode(evt.item)
      const newIndex = this.getVmIndexFromDomIndex(evt.newIndex)
      this.spliceList(newIndex, 0, element)
      const added = { element, newIndex }
      this.emitChanges({ added })
    },

    onDragRemove(evt) {
      insertNodeAt(this.$el, evt.item, evt.oldIndex)
      if (evt.pullMode === 'clone') {
        removeNode(evt.clone)
        return
      }
      const { index: oldIndex, element } = this.context
      this.spliceList(oldIndex, 1)
      const removed = { element, oldIndex }
      this.emitChanges({ removed })
    },

    onDragUpdate(evt) {
      removeNode(evt.item)
      insertNodeAt(evt.from, evt.item, evt.oldIndex)
      const oldIndex = this.context.index
      const newIndex = this.getVmIndexFromDomIndex(evt.newIndex)
      this.updatePosition(oldIndex, newIndex)
      const moved = { element: this.context.element, oldIndex, newIndex }
      this.emitChanges({ moved })
    },

    computeFutureIndex(relatedContext, evt) {
      if (!relatedContext.element) {
        return 0
      }
      const domChildren = [...evt.to.children].filter(
        (el) => el.style['display'] !== 'none'
      )
      const currentDomIndex = domChildren.indexOf(evt.related)
      const currentIndex =
        relatedContext.component.getVmIndexFromDomIndex(currentDomIndex)
      const draggedInList = domChildren.indexOf(draggingElement) !== -1
      return draggedInList || !evt.willInsertAfter
        ? currentIndex
        : currentIndex + 1
    },

    onDragMove(evt, originalEvent) {
      const { move, realList } = this
      if (!move || !realList) {
        return true
      }

      const relatedContext = this.getRelatedContextFromMoveEvent(evt)
      const futureIndex = this.computeFutureIndex(relatedContext, evt)
      const draggedContext = {
        ...this.context,
        futureIndex
      }
      const sendEvent = {
        ...evt,
        relatedContext,
        draggedContext
      }
      return move(sendEvent, originalEvent)
    },

    onDragEnd() {
      draggingElement = null
    }
  }
})

export default draggableComponent
